package view;

import javax.swing.*;

import controller.EditorController;
import controller.GameController;
import controller.GameControlCommand;

import java.awt.*;
import java.awt.event.*;

import javax.swing.ImageIcon;

import java.io.*;

import model.*;

/**
 * PacFrame class represents the main frame for the
 * GUI view of tb-pacman. This frame is designed to listen
 * on a single GameModel object.
 * 
 * @author Alexander Craig
 * @author Robert Gillespie
 */
@SuppressWarnings("serial")
public class PacApplet extends JApplet implements GameListener, EditorListener{
	
	private static final String HELP_MESSAGE = "To start a new game or restart, select one of the Play options from " +
							"the File menu.\n\nObjective: Help Pacman eat the required amount of dots " +
							"in a level and avoid the ghosts\n\nControls:  WASD to move\n                 " +
							"Q to quit\n                 E to wait in place\n" +
							"                 R to reset. Use this if you're about to be eaten!\n\nGhost Behaviour:\nBlue Ghosts: Favour horizontal movement" +
							"\nRed Ghosts: Favour vertical movement\nOrange Ghosts: Will select randomly when" +
							" two moves are equally valid";
	private static final String LEVEL_PACK_CREATOR = "Level Pack Creator";
	private static final String LEVEL_MUST_BE_LOADED_MSG = "A level must be loaded into the level editor before play testing.";
	private static final String NO_PLAYER_SPAWN_ERROR = "A Player spawn point must be specified before being loaded into the game.";
	private static final String LEVEL_COULD_NOT_BE_LOADED_MSG = "Level could not be loaded.";
	private static final String LEVEL_COULD_NOT_BE_READ_MSG = "Level file could not be read. Check for invalid extension (should be .ots)";
	private static final String ERROR_PANE_HEADER = "Error";
	private static final String LEVEL_PACK_FILE_EXTENSION = ".olp";
	private static final String LEVEL_PACK_COULD_NOT_BE_LOADED_MSG = "Level pack could not be loaded.";
	private static final String LEVEL_PACK_CANNOT_BE_OPEN_MSG = "Level pack file could not be read. Check for invalid extension (should be .olp)";
	private static final String STANDARD_LEVEL_PACK_LOCATION = "levels\\std.olp";
	private static final String STANDARD_LEVEL_PACK_NOT_FOUND_MSG = "The standard level pack cannot be found. Ensure that std.olp exists in the level directory.";
	public static final String NOT_AVAILABLE = "This feature is not available in the applet version as of this time. Please try the downloadable version!";
	public static final String NOT_AVAILABLE_TITLE = "Not Available";
	
	
	/** Extra screen space (in pixels) to be added on the sides of the maze currently being displayed */
	private static final int GAME_UI_HEIGHT = 110;
	private static final int GAME_UI_WIDTH = 30;
	private static final int EDITOR_UI_HEIGHT = 140;
	private static final int EDITOR_UI_WIDTH = 200;
	
	/** pixel offset used to compensate for the size of the menu/status bars when setting frame size */
	private static final int MENUS_SIZE_PX = 60;
	
	/** default font information */
	// private static final int DEFAULT_FONT_SIZE = 14; // Not used at the moment
	// private static final String DEFAULT_FONT = "Dialog";
	
	/** design Teams information. */
	private static final String TEAM_INFO = "\nBy Team Ortega Taco Sauce\nAlexander \"D'BrickShaw\" Craig\nAlexander \"Assjenkins\" Dinardo\nRobert \"Left-T\" Gillespie";
	
	/** text for the game menus */
	private static final String MENU_ABOUT = "About";
	private static final String MENU_SETTINGS_HIGH = "High";
	private static final String MENU_SETTINGS_MEDIUM = "Medium";
	private static final String MENU_SETTINGS_LOW = "Low";
	private static final String MENU_QUIT = "Quit";
	private static final String MENU_NEW_GAME = "Play Standard Level Pack";
	private static final String MENU_CUSTOM_LEVEL_PACK = "Play Custom Level Pack";
	private static final String MENU_CUSTOM_LEVEL = "Play Custom Level";
	private static final String MENU_LEVEL_EDIT = "Open Level Editor";
	private static final String MENU_PLAYTEST = "Playtest Editor Level";
	private static final String MENU_LEVELPACK_EDIT = "Create New Level Pack";
	private static final String MENU_HELP = "Help";
	private static final String MENU_SETTINGS = "Settings";
	private static final String MENU_FILE = "File";
	private static final String MENU_ADVANCED = "Advanced";
	
	/** error message for the event in which the look and feel for the OS fail to load*/
	private static final String MSG_LOOK_AND_FEEL_FAIL = "Unable to load default look and feel.";
	
	/** the games title */
	private static final String GAME_TITLE = "Pacman's Perilous Predicament";

	/** the version number to be displayed on the UI */
	private static final String VERSION_ID = "v1.02 (Applet Version)";
	
	/** the game model to be watched by this view */
	private GameModel model;
	/** the controller for the game model */
	private GameController modelControl;
	/** the level editor to be used by this frame */
	private LevelEditor editor;
	/** the controller for the editor */
	private EditorController editorControl;
	/** the canvas which draws the majority of the game information */
	private PacCanvas gameScreen;
	/** the label used to show splash screens and other information */
	private JLabel infoLabel;
	/** the panel used to contain additional components and display information about the level editor */
	private JPanel editorPanel;
	/** the panel used to access the level editor tools*/
	private JPanel editorToolbar;
	/** the LevelPackFrame used to create level packs */
	private LevelPackFrame packFrame;
	/** the playtest menu item (needs to be enabled/disabled in various places) */
	private JMenuItem playTest;
	
	
	/**
	 * Generates a new PacFrame listening on the given GameModel.
	 * Each PacFrame can only listen on one GameModel at a time, and
	 * will ignore all events from other sources.
	 * @param model	the GameModel to listen on
	 */
	public void init() {
		
		// Set look and feel
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
        catch (Exception e) {
           try {
                UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
            }
            catch (Exception e2) {
                System.err.println(MSG_LOOK_AND_FEEL_FAIL);
                System.exit(1);
            }
        }
        // Initialise Status Bar
        this.getContentPane().add(new JLabel(GAME_TITLE + " - " + VERSION_ID), BorderLayout.SOUTH);

        // Note: Required for menuBar and tool tips to properly overlap Canvas on some systems
        JPopupMenu.setDefaultLightWeightPopupEnabled(false);
        ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false);
        
		// Initialise menuBar
		JMenuBar menuBar = new JMenuBar();
		this.setJMenuBar(menuBar);
		
		// Initialise top level menus
		initFileMenu();
		initSettingsMenu();
		initAdvancedMenu();
		initHelpMenu();
		
		// Initialise splash screen
		ImageIcon icon = createImageIcon("images/PPP.png");
		infoLabel = new JLabel(icon);
		infoLabel.setBackground(Color.BLACK);
		infoLabel.setOpaque(true);
		this.getContentPane().add(infoLabel);
		
		packFrame = null;
		model = null;
		modelControl = null;
		editor = null;
		editorControl = null;

		this.setSize(new Dimension(icon.getIconWidth(), icon.getIconHeight() + MENUS_SIZE_PX));
		this.setMinimumSize(new Dimension(icon.getIconWidth(), icon.getIconHeight() + MENUS_SIZE_PX));
		this.setVisible(true);
	}
	
	/**
	 * Initialises the file menu, and adds it to the frame's menu bar.
	 */
	private void initFileMenu() {
		JMenu fileMenu = new JMenu(MENU_FILE);
		fileMenu.setMnemonic(KeyEvent.VK_F);
		this.getJMenuBar().add(fileMenu);
		
		// Standard Play Menu Option
		JMenuItem newStandard = new JMenuItem(MENU_NEW_GAME, KeyEvent.VK_N);
		newStandard.addActionListener(new ActionListener() {
			@Override
			/** Loads a new game using the standard level pack */
			public void actionPerformed(ActionEvent e) {
				// Generate the maze
                Maze testMaze = new Maze(11,6, true);
                // Top left wall
                testMaze.removeExit(1, 0, Direction.SOUTH, true);
                testMaze.removeExit(2, 0, Direction.SOUTH, true);
                testMaze.removeExit(3, 0, Direction.SOUTH, true);
                testMaze.removeExit(3, 0, Direction.EAST, true);
                testMaze.removeExit(1, 1, Direction.WEST, true);
                // Bottom wall
                testMaze.removeExit(1, 1, Direction.SOUTH, true);
                testMaze.removeExit(2, 1, Direction.SOUTH, true);
                testMaze.removeExit(3, 1, Direction.SOUTH, true);
                testMaze.removeExit(1, 2, Direction.WEST, true);
                testMaze.removeExit(5, 3, Direction.SOUTH, true);
                testMaze.removeExit(6, 3, Direction.SOUTH, true);
                testMaze.removeExit(7, 3, Direction.SOUTH, true);
                testMaze.removeExit(8, 3, Direction.SOUTH, true);
                testMaze.removeExit(9, 3, Direction.SOUTH, true);
                testMaze.removeExit(10, 3, Direction.WEST, true);
                testMaze.removeExit(10, 2, Direction.WEST, true);
                testMaze.removeExit(10, 1, Direction.WEST, true);
                // Middle wall
                testMaze.removeExit(5, 0, Direction.EAST, true);
                //testMaze.removeExit(5, 1, Direction.EAST, true);
                testMaze.removeExit(5, 2, Direction.EAST, true);
                testMaze.getTile(6, 5).setExit(Direction.SOUTH,
                                testMaze.getTile(10,3));
                testMaze.getTile(10, 2).setExit(Direction.EAST,
                                testMaze.getTile(0,3));
                testMaze.getTile(6, 1).removeExit(Direction.WEST);
                testMaze.getTile(6, 1).setExit(Direction.WEST,
                                testMaze.getTile(1,0));
                testMaze.removeTile(5, 5);
                
                testMaze.setPassDots(8);
                
                // Generate the level (player and ghost starts)
                GameLevel testLevel = new GameLevel(testMaze, "Applet Test Level");
                testLevel.setPlayerStart(testMaze.getTile(2, 3));
                testLevel.addSpawnPoint(testMaze.getTile(1, 1), GameEntity.GHOST_HORIZONTAL);
                testLevel.addSpawnPoint(testMaze.getTile(3, 0), GameEntity.GHOST_VERTICAL);

				freeModel();
				freeEditor();
				model = new GameModel(testLevel);
				model.addGameListener(PacApplet.this);
				setupGameScreen();
			}
		});
		fileMenu.add(newStandard);
		
		// Custom Level Pack Menu Option
		JMenuItem newPack = new JMenuItem(MENU_CUSTOM_LEVEL_PACK, KeyEvent.VK_P);
		newPack.addActionListener(new ActionListener() {
			@Override
			/** Prompts the user to select a level pack, and attempts to load a game using that level pack */
			public void actionPerformed(ActionEvent e) {
				JOptionPane.showMessageDialog(null, NOT_AVAILABLE, NOT_AVAILABLE_TITLE, JOptionPane.INFORMATION_MESSAGE);
			}
		});
		fileMenu.add(newPack);
		
		// Custom Level Menu Option
		JMenuItem newLevel = new JMenuItem(MENU_CUSTOM_LEVEL, KeyEvent.VK_L);
		newLevel.addActionListener(new ActionListener() {
			@Override
			/** Prompts the user to select a level pack, and attempts to load a game using that level pack */
			public void actionPerformed(ActionEvent e) {
				JOptionPane.showMessageDialog(null, NOT_AVAILABLE, NOT_AVAILABLE_TITLE, JOptionPane.INFORMATION_MESSAGE);
			}
		});
		fileMenu.add(newLevel);
		
		// Quit Menu Option
		JMenuItem quit = new JMenuItem(MENU_QUIT, KeyEvent.VK_Q);
		quit.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				// System.exit(0);
			}
		});
		fileMenu.add(quit);
	}
	
	/**
	 * Initialises the settings menu, and adds it to the frame's menu bar.
	 */
	private void initSettingsMenu() {
		JMenu settingsMenu = new JMenu(MENU_SETTINGS);
		settingsMenu.setMnemonic(KeyEvent.VK_S);
		this.getJMenuBar().add(settingsMenu);

		// Low Settings Menu Option
		JMenuItem sLow = new JMenuItem(MENU_SETTINGS_LOW, KeyEvent.VK_L);
		sLow.addActionListener(new ActionListener() {
			/** Sets the PacCanvas rendering settings to low */
			public void actionPerformed(ActionEvent e) {
				if( gameScreen != null ) {
					gameScreen.setSettings(PacCanvas.SETTINGS_LOW);
				}
			}
		});
		settingsMenu.add(sLow);
		
		// Mid Settings Menu Option
		JMenuItem sMid = new JMenuItem(MENU_SETTINGS_MEDIUM, KeyEvent.VK_M);
		sMid.addActionListener(new ActionListener() {
			/** Sets the PacCanvas rendering settings to medium */
			public void actionPerformed(ActionEvent e) {
				if( gameScreen != null ) {
					gameScreen.setSettings(PacCanvas.SETTINGS_MID);
				}
			}
		});
		settingsMenu.add(sMid);
		
		// High Settings Menu Option
		JMenuItem sHigh = new JMenuItem(MENU_SETTINGS_HIGH, KeyEvent.VK_H);
		sHigh.addActionListener(new ActionListener() {
			/** Sets the PacCanvas rendering settings to high */
			public void actionPerformed(ActionEvent e) {
				if( gameScreen != null ) {
					gameScreen.setSettings(PacCanvas.SETTINGS_HIGH);
				}
			}
		});
		settingsMenu.add(sHigh);
	}
	
	/**
	 * Initialises the advanced menu, and adds it to the frame's menu bar.
	 */
	private void initAdvancedMenu() {
		JMenu advancedMenu = new JMenu(MENU_ADVANCED);
		advancedMenu.setMnemonic(KeyEvent.VK_A);
		this.getJMenuBar().add(advancedMenu);
		
		// Level Editor Menu Option
		JMenuItem levelEdit = new JMenuItem(MENU_LEVEL_EDIT, KeyEvent.VK_O);
		/** Open Level Editor Action Listener */
		levelEdit.addActionListener(new ActionListener() {
			
			@Override
			/** 
			 * Sets the gameScreen to be a PacEditorCanvas, and sets up the
			 * required LevelEditor and EditorController.
			 */
			public void actionPerformed(ActionEvent e) {
				if (editor == null) {
					PacApplet.this.clearScreenAssets();
					editor = new LevelEditor();
					gameScreen = new PacEditorCanvas(editor);
					editorControl = new EditorController(editor, (PacEditorCanvas)gameScreen);
					editor.addEditorListener(PacApplet.this);
					editorPanel = new EditorPanel(editor, editorControl);
					editorToolbar = new EditorToolbar(editor, editorControl);
					if(model != null) {
						model.getMaze().resetMaze();
						editor.setLevel(model.getLevel());
						freeModel();
					}
					
					playTest.setEnabled(true);
					PacApplet.this.getContentPane().add(gameScreen, BorderLayout.CENTER);
					PacApplet.this.getContentPane().add(editorPanel, BorderLayout.EAST);
					PacApplet.this.getContentPane().add(editorToolbar, BorderLayout.NORTH);
					PacApplet.this.validate();
					PacApplet.this.repaint();
					PacApplet.this.gameScreen.repaint();
					PacApplet.this.setVisible(true);
				}
			}
			
		});
		advancedMenu.add(levelEdit);
		
		// Play Test Menu Option
		playTest = new JMenuItem(MENU_PLAYTEST, KeyEvent.VK_P);
		playTest.setActionCommand(GameControlCommand.NEW_GAME.toString());
		playTest.addActionListener(new ActionListener() {
			@Override
			/** 
			 * Sets the gameScreen to be a PacGameCanvas, and sets up the
			 * required GameModel and GameController.
			 */
			public void actionPerformed(ActionEvent e) {
				
				boolean error = false;
				if(editor != null && editor.getLevel() != null) {
					if(editor.hasValidLevel()){
						model = new GameModel(editor.getLevel());
					}else{
						error = true;
						JOptionPane.showMessageDialog(PacApplet.this,
								NO_PLAYER_SPAWN_ERROR,
								ERROR_PANE_HEADER, JOptionPane.ERROR_MESSAGE);
					}
				} else {
					error = true;
					JOptionPane.showMessageDialog(PacApplet.this,
							LEVEL_MUST_BE_LOADED_MSG,
							ERROR_PANE_HEADER, JOptionPane.ERROR_MESSAGE);
				}
				
				if(!error){
					freeEditor();
					setupGameScreen();
				}
			}
		});
		advancedMenu.add(playTest);
		playTest.setEnabled(false);
	}
	
	/**
	 * Initialises the help menu, and adds it to the frame's menu bar.
	 */
	private void initHelpMenu() {
		JMenu helpMenu = new JMenu(MENU_HELP);
		helpMenu.setMnemonic(KeyEvent.VK_H);
		this.getJMenuBar().add(helpMenu);	
		
		// Initialise help menu
		JMenuItem about = new JMenuItem(MENU_ABOUT, KeyEvent.VK_A);
		about.addActionListener(new ActionListener() {
			/** Shows an "about" dialog when the button is pressed */
			public void actionPerformed(ActionEvent arg0) {
				JOptionPane.showMessageDialog(PacApplet.this,
					    GAME_TITLE + " - " + VERSION_ID + TEAM_INFO,
					    MENU_ABOUT, JOptionPane.INFORMATION_MESSAGE);

			}
		});
		helpMenu.add(about);
		
		JMenuItem instructions = new JMenuItem("Instructions", KeyEvent.VK_I);
		instructions.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e){
				JOptionPane.showMessageDialog(null, HELP_MESSAGE,
						"TB-Pacman Help", JOptionPane.INFORMATION_MESSAGE);
			}
		});
		helpMenu.add(instructions);
	}
	
	/**
	 * Ensures that the gameScreen, infoLabel, editorToolbar, and editorPanel are clear so a new state can be loaded.
	 */
	private void clearScreenAssets() {
		if (gameScreen != null) {
			if(gameScreen instanceof PacGameCanvas && model != null) 
				model.removeGameListener((PacGameCanvas)gameScreen);
			if(gameScreen instanceof PacEditorCanvas && editor != null) 
				editor.removeEditorListener((PacEditorCanvas)gameScreen);
			gameScreen.removeKeyListener(modelControl);
			gameScreen.removeKeyListener(editorControl);
			gameScreen.removeMouseListener(editorControl);
			gameScreen.freeResources();
			gameScreen = null;
		}
		if (editorPanel != null) {
			editorPanel = null;
		}
		if (editorToolbar != null) {
			editorToolbar = null;
		}
		if (infoLabel != null) {
			infoLabel = null;
		}
		
		getContentPane().removeAll();
	}
	
	/**
	 * Frees all references to/from the model and model controller
	 */
	private void freeModel() {
		if (model != null) {
			model.removeAllListeners();
		}
		model = null;
		modelControl = null;
	}
	
	/**
	 * Frees all references to/from the editor and editor controller
	 */
	private void freeEditor() {
		if(editor != null) {
			editor.removeAllListeners();
		}
		editor = null;
		editorControl = null;
		playTest.setEnabled(false);
	}
	
	/**
	 * Sets up the game area to display the currently loaded model.
	 */
	private void setupGameScreen() {
		clearScreenAssets();
		gameScreen = new PacGameCanvas(model);
		modelControl = new GameController(model, (PacGameCanvas)gameScreen);
		getContentPane().add(gameScreen, BorderLayout.CENTER);
		modelControl.startNewGame();
		setVisible(true);
		gameScreen.requestFocus();
		gameScreen.validate();
		gameScreen.repaint();
		validate();
		repaint();
	}
	
	/** Returns an ImageIcon, or null if the path was invalid. */
	private ImageIcon createImageIcon(String path) {
	    java.net.URL imgURL = getClass().getResource(path);
	    if (imgURL != null) {
	        return new ImageIcon(imgURL, "");
	    } else {
	        System.err.println("Couldn't find file: " + path);
	        return null;
	    }
	}
	
	@Override
	/**
	 * Sets the frame size whenever a new level is loaded into the editor
	 */
	public void newLevel(EditorEvent event) {
		LevelEditor editor = event.getSource();
		this.setMinimumSize(new Dimension(editor.getMaze().getMaxX()*(int)PacCanvas.TILE_SPREAD + EDITOR_UI_WIDTH
				,editor.getMaze().getMaxY()*(int)PacCanvas.TILE_SPREAD + EDITOR_UI_HEIGHT + MENUS_SIZE_PX));
		
	}
	
	@Override
	/**
	 * Sets the frame size whenever a new level is loaded into the game model
	 */
	public void levelReset(GameEvent event) {
		GameModel model = event.getSource();
		this.setMinimumSize(new Dimension(model.getMaze().getMaxX()*(int)PacCanvas.TILE_SPREAD + GAME_UI_WIDTH
				,model.getMaze().getMaxY()*(int)PacCanvas.TILE_SPREAD + GAME_UI_HEIGHT + MENUS_SIZE_PX));
	}
	
	

	@Override
	public void newGame(GameEvent event) {}
	@Override
	public void gameOver(GameEvent event) {}
	@Override
	public void levelComplete(GameEvent event) {}
	@Override
	public void playerDeath(GameEvent event) {}
	@Override
	public void playerMove(GameEvent event) {}
	@Override
	public void mazeChange(EditorEvent event) {}
	@Override
	public void selectionChange(EditorEvent event) {}
	@Override
	public void toolChange(EditorEvent event) {}
}
